问题描述

业务中有用到一个驱动,但是加载这个驱动并使用他的一些功能时,会导致设备宕机重启。查看内核日志和转储宕机日志,发现时触发了内核软狗。

1
2
cat /var/log/messages
cat /var/crash/crash.txt

查看当前系统的看门狗超时时间,发现是5S,这个值一般默认是10S,直接改大这个超时时间到30S后成功解决问题:

1
2
3
4
# 查看看门狗时间
cat /proc/sys/kernel/watchdog_thresh
# 改大
echo 30 > /proc/sys/kernel/watchdog_thresh

至于这个参数,其实该参数控制的是 看门狗超时阈值,即在系统没有发送内核心跳信号(或者系统处于“无响应”状态)超过此时间时,会触发看门狗机制。这个参数的值通常是以秒为单位,表示系统多久没有响应后,触发看门狗行为。

再次使用驱动,执行相关命令,并监视CPU占用,发现CPU有个核心在一段时间内占用达到了100%,分析确定是驱动存在相关问题。

问题分析

驱动上加了一些内核日志,最终定位到是有段代码存在问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// .......
long err_timeout = jiffies + HZ*60;
long spin_timeout = jiffies + HZ*5;

do{
int status = get_status();
if(status == OK){
return 0;
}
if(is_after(jiffies, err_timeout)){
printk("ERROR.....");
return ERROR;
}else if(is_after(jiffies, spin_timeout)){
msleep(100);
}else{
_mm_pause();
}

}while(1);

// ....

这段代码逻辑很清楚,就是有两个超时时间,在spin_timeout内,不释放CPU,自旋的获取状态,超过时间后就进入阻塞式获取状态,最终还不行就超时报错。

逻辑确实没问题,但是坏就坏在自旋超时时间设置的太大了!自旋的适合并不会释放CPU,而这里有没有阻塞的IO操作,于是导致CPU被占用到100,进而触发了内核软狗,导致系统重启!

那么解决其实就很好解决了,直接把自旋的时间缩小就行了。至于缩小到什么程度,还是要看你的业务实现,太短了自旋没啥意义,太大了导致你CPU被冗余占用。

问题发散

这个问题让我想起前两个月写的关于CPU加压的那篇文章,现在反过来同样有几个思考。

忙等待与自旋

忙等待(Busy Waiting)

忙等待是指程序在等待某个条件满足时,通过不断检查条件的状态来决定是否继续执行。这种等待方式没有让出 CPU 的控制权,而是一直占用 CPU,导致资源的浪费。

特征

  1. 程序在循环中反复检查条件。
  2. 不会主动放弃 CPU 资源。
  3. 会导致 CPU 使用率升高,即使没有实际的任务在执行。

示例

1
2
3
while (!condition) {
// 什么都不做,只检查条件
}

在上述代码中,程序会持续循环检查 condition,直到条件满足。期间 CPU 一直被占用。


自旋(Spin Waiting 或 Spinning)

自旋是忙等待的一种具体实现方式,通常在 多线程编程中用于尝试获取锁或其他共享资源。当一个线程尝试获取锁时,如果锁已经被其他线程占用,自旋线程会在循环中反复检查锁是否可用。

特征

  1. 类似于忙等待,但有更明确的目的(例如,获取锁)。
  2. 自旋通常用于 短时间的等待,避免线程上下文切换的开销。
  3. 常常在多核 CPU 系统中使用,因为其他核可能很快释放资源。

示例

1
2
3
4
while (lock_is_taken) {
// 自旋等待锁释放
_mm_pause(); // 用于减少对总线和缓存的占用
}

在这个例子中,线程会在 lock_is_taken 变为 false 时退出循环,获取锁。


忙等待与自旋的主要区别

对比点 忙等待 自旋
目的 一般用于等待某个条件,无具体场景约束 用于等待锁或资源,主要用于多线程场景
适用场景 无明确目的的循环等待 资源预期很快释放的短时间等待
CPU 占用 持续占用 CPU,浪费资源 设计更优化,比如使用 _mm_pause 等减轻 CPU 压力
是否主动让出 CPU 通常不会 通常不会,但有些实现可能会结合让步策略
多核支持 适用性较低,可能浪费资源 常在多核 CPU 上运行,多个线程分布在不同核心上

适用场景

忙等待

  • 一般不推荐使用,除非在一些非常特殊的场景下,比如某些嵌入式系统中没有多线程调度的支持。

自旋

  • 多核场景:当资源预计很快释放时,用自旋可以避免线程上下文切换的开销。
  • 短等待场景:如果锁的持有时间非常短,自旋更高效。
  • 长等待场景:如果等待时间较长,自旋可能会浪费 CPU 资源,此时应考虑使用 阻塞

优化措施

  • 忙等待中尽量避免无意义的循环。例如,在等待时加入休眠或暂停(如 sleepsched_yield_mm_pause)。

    示例:

    1
    2
    3
    while (!condition) {
    sched_yield(); // 暂时让出 CPU
    }
  • 自旋中可以使用专用指令(如 _mm_pause)降低资源占用:

    1
    2
    3
    while (!condition) {
    _mm_pause(); // 减少总线和缓存占用
    }

总结

  1. 忙等待:更通用但浪费资源,适用性较低。
  2. 自旋:忙等待的优化版本,主要在短时间等待锁或资源的多核场景中使用。
  3. 如果等待时间预计较长,应考虑通过 阻塞 机制(如 mutexcondition variable)来让出 CPU,减少资源浪费。

CPU密集型和IO密集型运算

I/O密集型CPU密集型是描述程序或任务执行时主要受限于哪类资源的术语。它们分别对应系统中两种主要的瓶颈:I/O操作(如磁盘、网络)和CPU运算(如数学计算、数据处理)。


1. I/O密集型

I/O密集型程序是指程序的大部分时间花在等待输入/输出设备完成操作上,而不是执行计算任务。

特点

  • 主要瓶颈:I/O设备的速度,例如磁盘读写、网络数据传输。
  • CPU使用率:通常较低,因为 CPU 大部分时间在等待 I/O 设备完成任务。
  • 常见场景
    • 数据库操作
    • 文件读写
    • 网络请求(例如 Web 服务)
    • 日志处理

优化策略

  • 异步 I/O:通过异步或非阻塞 I/O 减少等待时间,例如使用多线程或事件驱动模型。
  • 并发处理:增加线程或进程数,提高对多个 I/O 操作的处理能力。
  • 缓存优化:通过缓存减少对慢速 I/O 设备的访问。
  • 负载均衡:对于网络 I/O,合理分配任务以避免单点过载。

示例

一个从网络上读取文件并保存到磁盘的程序:

1
2
3
4
5
6
import requests

def download_file(url, filename):
response = requests.get(url) # 网络 I/O
with open(filename, 'wb') as file:
file.write(response.content) # 磁盘 I/O

2. CPU密集型

CPU密集型程序是指程序的大部分时间花在执行计算任务上,而不是等待其他设备完成任务。

特点

  • 主要瓶颈:CPU 运算能力,例如复杂算法、大量数学计算、图像处理等。
  • CPU使用率:通常很高,因为 CPU 一直在执行计算任务。
  • 常见场景
    • 加密解密
    • 视频/音频处理
    • 数据分析与机器学习模型训练
    • 物理模拟(例如计算流体动力学)

优化策略

  • 多线程/多进程:在多核 CPU 上并行执行任务。
  • 算法优化:优化算法复杂度,减少不必要的计算。
  • 硬件加速:利用 GPU 或专用硬件(如 TPU、FPGA)加速计算。
  • 代码优化:使用高效的编程语言或库(如 C++、NumPy)。

示例

计算斐波那契数列的程序(递归版本):

1
2
3
4
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2) # CPU 密集型计算

3. 区别总结

属性 I/O密集型 CPU密集型
主要瓶颈 I/O设备(磁盘、网络等)的速度 CPU 的计算能力
CPU使用率 通常较低,大部分时间等待 I/O 操作完成 通常较高,CPU 持续工作
线程并发 高并发有助于掩盖 I/O 等待 高并发可能导致线程争夺 CPU(需限制线程数)
优化方向 异步 I/O、并发、缓存等 算法优化、多线程、硬件加速
常见场景 文件操作、数据库访问、网络请求 加密解密、数据分析、图像处理

4. 综合场景

许多应用程序是 I/O密集型CPU密集型 的混合体。例如:

  • 一个大型 Web 应用可能在处理用户请求时需要进行数据库查询(I/O密集型),同时对返回的数据进行分析和格式化(CPU密集型)。
  • 视频编辑软件需要从磁盘加载视频数据(I/O密集型),同时对视频帧进行编码或滤波处理(CPU密集型)。

对于这样的混合场景,需要同时优化 I/O 和 CPU 的使用效率,合理分配资源。


总结

  1. I/O密集型:主要受限于输入/输出设备的性能(网络、磁盘等)。
  2. CPU密集型:主要受限于处理器的计算能力。
  3. 优化方法:针对不同的瓶颈采取相应的策略,避免资源浪费或系统瓶颈影响性能。

_mm_pause/cpu_relax作用

_mm_pausecpu_relax 是用于优化自旋锁或者忙等待的指令或函数。它们的主要目的是降低忙等待过程中对系统的资源占用,同时提高性能和多核 CPU 的协作效率。


1. _mm_pause

概述

_mm_pause 是 Intel 提供的一个内联函数,它本质上是对 PAUSE 汇编指令的封装。PAUSE 是一种提示指令,告诉 CPU 当前的循环是忙等待,让 CPU 优化其内部操作。

作用

  1. 降低功耗
    • 在忙等待期间,CPU执行无意义的循环会浪费功耗,_mm_pause 指令可以减少这种浪费。
  2. 减少总线争用
    • 在多核 CPU 系统中,忙等待可能导致缓存一致性协议占用大量总线带宽,PAUSE 可以减少这种影响。
  3. 避免退化
    • 超标量 CPU 执行紧密循环时可能会发生管道退化(Pipeline Stall),PAUSE 指令可以提示 CPU 延迟循环,从而提高管道效率。

示例

1
2
3
4
5
6
7
8
#include <emmintrin.h>

void spin_lock(int *lock) {
while (__sync_lock_test_and_set(lock, 1)) {
// 自旋等待
_mm_pause();
}
}

适用场景

  • 多线程的自旋锁。
  • 短时间的忙等待。
  • 多核系统下避免不必要的资源争用。

2. cpu_relax

概述

cpu_relax 是 Linux 内核中的一个宏,它在不同的架构下提供了统一的自旋等待优化方式:

  • 在 x86/x86_64 平台,它通常等价于 _mm_pause
  • 在 ARM 平台,它可能对应 yield 或者其他低功耗的等待指令。
  • 在未提供特定实现的平台,它可能被定义为空操作(nop)。

作用

cpu_relax 的功能与 _mm_pause 类似,其核心目的也是优化忙等待,具体包括:

  1. 减少功耗和资源争用
    • 提示 CPU 当前是一个低优先级循环,不需要激进执行。
  2. 平台无关性
    • 提供统一的接口,屏蔽不同平台间的指令差异。

示例

Linux 内核中的自旋锁实现:

1
2
3
while (lock_is_held()) {
cpu_relax(); // 优化自旋等待
}

3. _mm_pausecpu_relax 的区别

属性 _mm_pause cpu_relax
适用范围 主要在 x86/x86_64 平台使用 跨平台支持,多种 CPU 架构
实现方式 调用 Intel 的 PAUSE 指令 由架构具体实现,可能是 PAUSE 或其他指令
库/环境 依赖 Intel SSE2 指令集 Linux 内核通用宏
场景 用户态和内核态都可以使用 主要在内核态使用

4. _mm_pause 的性能优势

减少缓存争用

当多个线程同时在等待一个共享资源(例如锁)时,频繁检查变量会导致缓存行冲突。_mm_pause 会降低对变量的反复访问频率,从而缓解这种冲突。

避免管道阻塞

CPU 的流水线机制在执行紧密循环时可能因分支预测错误或其他因素导致停顿。使用 PAUSE 指令会让流水线重新填充,提高执行效率。

优化功耗

CPU 在执行 PAUSE 指令时会切换到一种低功耗模式,降低电量消耗,同时释放资源给其他线程。


5. 示例对比

普通忙等待

1
2
3
while (condition_not_met()) {
// 空循环,无优化
}
  • 高 CPU 占用。
  • 浪费电量和资源。
  • 导致缓存争用。

加入 _mm_pausecpu_relax 的优化版本

1
2
3
4
5
while (condition_not_met()) {
_mm_pause(); // x86/x86_64 平台
// 或者
cpu_relax(); // 通用平台
}
  • 降低对 CPU 资源的占用。
  • 缓解缓存争用和总线压力。
  • 提高系统多线程的协作效率。

6. 注意事项

  • 适用场景:仅适用于短时间的等待。例如资源即将释放的情况下。如果等待时间较长,应使用阻塞机制(如信号量、条件变量)。
  • 硬件依赖_mm_pause 依赖于支持 SSE2 指令集的 CPU,cpu_relax 则提供更广泛的兼容性。
  • 滥用风险:如果滥用忙等待,即使使用 _mm_pausecpu_relax,仍可能导致资源浪费。

7. 总结

  • _mm_pausecpu_relax 是忙等待优化的工具,特别适合多线程环境下的自旋锁或短期等待。
  • 它们通过提示 CPU 延迟循环,减少功耗、缓存争用和流水线退化,显著提高系统性能。
  • 在实际开发中,合理选择等待机制(自旋 vs 阻塞)和优化工具对于系统稳定性和性能至关重要。

Java中的自旋锁

先说结论,Java没有直接提供能用的自旋的锁,但是有几点。

  1. 能够通过原子类提供的CAS自己显示简单的自旋锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.util.concurrent.atomic.AtomicBoolean;

    public class SpinLock {
    private final AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
    while (!lock.compareAndSet(false, true)) {
    Thread.yield(); // 避免完全占用 CPU
    }
    }

    public void unlock() {
    lock.set(false);
    }
    }

    实现可重入自旋锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import java.util.concurrent.atomic.AtomicReference;

    public class ReentrantSpinLock {
    private final AtomicReference<Thread> owner = new AtomicReference<>();
    private int count = 0;

    public void lock() {
    Thread current = Thread.currentThread();
    if (owner.get() == current) {
    count++;
    return;
    }
    while (!owner.compareAndSet(null, current)) {
    Thread.yield();
    }
    }

    public void unlock() {
    Thread current = Thread.currentThread();
    if (owner.get() == current) {
    if (count > 0) {
    count--;
    } else {
    owner.set(null);
    }
    }
    }
    }
  2. 可重入锁ReentrantLock存在自旋现象

    查看底层的AQS源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    /**
    * Main acquire method, invoked by all exported acquire methods.
    *
    * @param node null unless a reacquiring Condition
    * @param arg the acquire argument
    * @param shared true if shared mode else exclusive
    * @param interruptible if abort and return negative on interrupt
    * @param timed if true use timed waits
    * @param time if timed, the System.nanoTime value to timeout
    * @return positive if acquired, 0 if timed out, negative if interrupted
    */
    final int acquire(Node node, int arg, boolean shared,
    boolean interruptible, boolean timed, long time) {
    Thread current = Thread.currentThread();
    byte spins = 0, postSpins = 0; // retries upon unpark of first thread
    boolean interrupted = false, first = false;
    Node pred = null; // predecessor of node when enqueued

    /*
    * Repeatedly:
    * Check if node now first
    * if so, ensure head stable, else ensure valid predecessor
    * if node is first or not yet enqueued, try acquiring
    * else if queue is not initialized, do so by attaching new header node
    * resort to spinwait on OOME trying to create node
    * else if node not yet created, create it
    * resort to spinwait on OOME trying to create node
    * else if not yet enqueued, try once to enqueue
    * else if woken from park, retry (up to postSpins times)
    * else if WAITING status not set, set and retry
    * else park and clear WAITING status, and check cancellation
    */

    for (;;) {
    if (!first && (pred = (node == null) ? null : node.prev) != null &&
    !(first = (head == pred))) {
    if (pred.status < 0) {
    cleanQueue(); // predecessor cancelled
    continue;
    } else if (pred.prev == null) {
    Thread.onSpinWait(); // ensure serialization
    continue;
    }
    }
    if (first || pred == null) {
    boolean acquired;
    try {
    if (shared)
    acquired = (tryAcquireShared(arg) >= 0);
    else
    acquired = tryAcquire(arg);
    } catch (Throwable ex) {
    cancelAcquire(node, interrupted, false);
    throw ex;
    }
    if (acquired) {
    if (first) {
    node.prev = null;
    head = node;
    pred.next = null;
    node.waiter = null;
    if (shared)
    signalNextIfShared(node);
    if (interrupted)
    current.interrupt();
    }
    return 1;
    }
    }
    Node t;
    if ((t = tail) == null) { // initialize queue
    if (tryInitializeHead() == null)
    return acquireOnOOME(shared, arg);
    } else if (node == null) { // allocate; retry before enqueue
    try {
    node = (shared) ? new SharedNode() : new ExclusiveNode();
    } catch (OutOfMemoryError oome) {
    return acquireOnOOME(shared, arg);
    }
    } else if (pred == null) { // try to enqueue
    node.waiter = current;
    node.setPrevRelaxed(t); // avoid unnecessary fence
    if (!casTail(t, node))
    node.setPrevRelaxed(null); // back out
    else
    t.next = node;
    } else if (first && spins != 0) {
    --spins; // reduce unfairness on rewaits
    Thread.onSpinWait();
    } else if (node.status == 0) {
    node.status = WAITING; // enable signal and recheck
    } else {
    long nanos;
    spins = postSpins = (byte)((postSpins << 1) | 1);
    if (!timed)
    LockSupport.park(this);
    else if ((nanos = time - System.nanoTime()) > 0L)
    LockSupport.parkNanos(this, nanos);
    else
    break;
    node.clearStatus();
    if ((interrupted |= Thread.interrupted()) && interruptible)
    break;
    }
    }
    return cancelAcquire(node, interrupted, interruptible);
    }

    可见在AQS中,在某些一些情况下,会优先进入自旋的状态而不是直接阻塞,同时在一轮的自旋后,还对自旋进行了时间增强(见postSpins逻辑)。至于AQS是如何具体进行自旋和阻塞切换的,以及如何具体唤醒线程取获取锁,讲起来又是一堆,下次有空再开一篇来说。