作者: Zeeko

  • 似乎修复了唤醒后键盘短暂失效的问题

    之前一篇文章的评论区有人提到过机械革命15XPro 暴风雪在唤醒后键盘会短暂失效的问题,往往需要重新睡眠再唤醒才能激活键盘。经过我的排查后发现,在该问题出现时,有两个比较重要的 Log 线索:

    Aug 27 09:04:52 arch kernel: atkbd serio0: Failed to deactivate keyboard on isa0060/serio0
    Aug 27 09:04:52 zarch kernel: atkbd serio0: Failed to enable keyboard on isa0060/serio0

    根据 Claude 以及互联网上相关的讨论,这个问题大概率跟内置键盘的驱动有关,通常可以通过尝试不同的 i8042/atkbd 相关参数来解决。

    跟 Claude 讨论一番过后,我决定添加如下 kernel parameters:

    i8042.reset=1 i8042.nomux=1 i8042.nopnp i8042.noloop atkbd.reset=1

    实际体验下来,键盘失效的问题已经一周多都没有复现了。

  • 性能与公平:解决 Linux 桌面无响应的取舍之道

    已经记不得从什么时候开始,我的 Linux 桌面偶尔会遇到无响应的问题,这个问题经常跟比较重的磁盘读写相关,例如更新 JetBrains IDE,JetBrains IDE 创建索引。之所以会把无响应的问题归结到磁盘读写上,是因为卡顿往往跟内存占用、CPU 使用率、进程数无关。卡顿的表现为:鼠标可以移动,但是 KDE 桌面包括状态栏无法更新;Kitty 可以创建新的 Tab,但是 Fish 启动会卡住;已有终端下虽然可以流畅输入命令,但是执行命令会卡住。这个周末因为成都一直在下雨,不太方便出门,决定来尝试解决卡顿问题。

    我的电脑配置如下:

    出现问题的固态硬盘型号是 KINGSTON SNVS1000G, SMART 信息如下:

    # nvme smart-log /dev/nvme1
    Smart Log for NVME device:nvme1 namespace-id:ffffffff
    critical_warning			: 0
    temperature				: 89 °F (305 K)
    available_spare				: 100%
    available_spare_threshold		: 10%
    percentage_used				: 31%
    endurance group critical warning summary: 0
    Data Units Read				: 61665672 (31.57 TB)
    Data Units Written			: 171536497 (87.83 TB)
    host_read_commands			: 967548759
    host_write_commands			: 2052988968
    controller_busy_time			: 75703
    power_cycles				: 79989
    power_on_hours				: 15798
    unsafe_shutdowns			: 241
    media_errors				: 0
    num_err_log_entries			: 0
    Warning Temperature Time		: 0
    Critical Composite Temperature Time	: 0
    Thermal Management T1 Trans Count	: 0
    Thermal Management T2 Trans Count	: 0
    Thermal Management T1 Total Time	: 0
    Thermal Management T2 Total Time	: 0

    官网上查到这块固态硬盘的设计 TBW 是 240TB,目前写入了 87.83TB,按照这篇文章的估算方法,这块硬盘正值壮年,属于如果挂掉了会令人惋惜的状态。既然硬盘是健康的,那么就需要从软件的角度来排查问题了。

    复现问题

    根据我的日常使用经验,卡顿往往伴随高磁盘 IO,故可以使用硬盘跑分工具测试来复现问题:

    fio --filename=/path/to/no_cow_test.fio --size=8GB --direct=1 --rw=randrw --bs=4k --ioengine=libaio --iodepth=256 --runtime=120 --numjobs=4 --time_based --group_reporting --name=iops-test-job --eta-newline=1

    fio 运行时,可以通过 htop 的 IO Tab 看到有明显的磁盘读写,这时如果运行不太常用的命令,比如前端切图仔电脑上的 rustc --version,就可以观察到明显的延迟。通过这种方式比较稳定地复现问题场景后,就可以继续接下来的排查了。

    检查日志

    journalctl -b -p err --no-pagerdmsg -T 展示的错误日志通常对排查问题非常重要,但是在我的卡顿问题复现时却没有记录下任何错误信息。问题的排查一度陷入僵局,这时就得靠 Gemini 大师帮我排查一下有哪些设置会影响存储设备的性能了。

    脏页

    在 Linux 系统中,当应用程序向文件写入数据时,这些数据通常不会立即被写入到物理磁盘上。相反,它们首先被写入到内存中的一个缓冲区(称为「页缓存」或「page cache」)。这些已经修改但尚未写入磁盘的内存页就被称为「脏页」。

    脏页机制可以有效提升磁盘性能,但为了防止脏页无限增长,我们可以设定 vm.dirty_ratio 参数限制脏页数据占总内存的百分比,更多介绍请看 Wiki

    对于我的配置而言,如果 dirty_ratio 过大,比如 20%,意味着脏页数据量最高达到 6GB,不仅会挤占其他应用的可用内存空间,还可能由于操作系统需要一次性写入大量数据造成卡顿。

    但在我的测试下,将 vm.ditry_ratio 以及相关参数调小一些带来的效果并不明显,所以还得向其他的方向上挖掘。

    IO 调度器

    IO 调度器是 Linux 内核的一个组件,它的主要任务是优化对存储设备(如硬盘、SSD、NVMe)的读写请求顺序。

    对于 SSD 设备来说,IO 调度器可以帮助确保每个进程都能获得合理的 IO 访问机会,避免某些进程被「饿死」,另外不同的调度算法会在数据吞吐量与请求延迟之间有不同的取舍,以适应不同的应用场景。

    在默认情况下,Linux 不会为 NVME 设备设置 IO 调度器,它假设这类设备上的控制器有自己的优化机制,不需要额外的软件层面调度。我们可以通过下面的命令查看当前在使用的调度器(被方括号包围):

    cat /sys/block/nvme1n1/queue/scheduler
    [none] mq-deadline kyber bfq

    然而在调度器的选择上,Gemini 跟 Arch Wiki 出现了分歧,Gemini 认为 NVME 以及 SSD 就应该选择 none 作为 io scheduler,但 Arch Wiki 认为 it depends

    The best choice of scheduler depends on both the device and the exact nature of the workload. Also, the throughput in MB/s is not the only measure of performance: deadline or fairness deteriorate the overall throughput but may improve system responsiveness. Benchmarking may be useful to indicate each I/O scheduler performance.

    考虑到我的日常使用场景是桌面应用,NVME 上自带的控制器完全不可能知道我正在使用的窗口应用,也就没法保障我主要使用场景下的响应速度,于是我决定采用 Pop_OS 的推荐,使用 Kyber 调度器。

    重试问题场景后,卡顿的情况缓解了一些,但跟我的预期还有很大偏差,很多命令可能需要 10s 以上才能输出结果。于是又尝试了使用 BFQ scheduler,终于 rustc --version 可以在后台运行重 IO 程序的情况下在 1s 内输出结果了。使用下面的 udev rule 就可以持久化设置 IO 调度器。

    # /etc/udev/rules.d/60-ioschedulers.rules
    ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="bfq"

    通过设置 IO 调度器很好的解决了我的问题,那么代价是什么呢?

    I/O 调度器性能对比

    相较于 none 调度器,其他的软件调度器都加入了额外的一层抽象,这势必会引入更多的开销,为了搞清楚降低延迟的开销,我进行了下面的测试。

    fio --filename=/path/to/no_cow/test.fio --size=8GB --direct=1 --rw=randrw --bs=4k --ioengine=libaio --iodepth=256 --runtime=20 --numjobs=4 --time_based --group_reporting --name=iops-test-job --eta-newline=1
    iops-test-job: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=256
    指标none(基准)BFQkyber
    读取 IOPS7,2147,018 (-2.7%)7,141 (-1.0%)
    写入 IOPS7,2357,039 (-2.7%)7,161 (-1.0%)
    总 IOPS14,44914,057 (-2.7%)14,302 (-1.0%)
    读取带宽28.2 MiB/s27.4 MiB/s (-2.8%)27.9 MiB/s (-1.1%)
    写入带宽28.3 MiB/s27.5 MiB/s (-2.8%)28.0 MiB/s (-1.1%)
    吞吐量性能指标
    延迟指标none(基准)BFQkyber
    读取平均延迟70.5ms72.4ms (+2.7%)71.2ms (+1.0%)
    写入平均延迟70.9ms72.9ms (+2.8%)71.7ms (+1.1%)
    50% 延迟70.8ms71ms (+0.3%)71ms (+0.3%)
    95% 延迟78ms89ms (+14.1%)81ms (+3.8%)
    99% 延迟82ms114ms (+39.0%)91-92ms (+11%)
    99.9% 延迟88ms132-133ms (+50.0%)128ms (+45%)
    延迟性能对比

    可以看到在单个进程的读写测试下, BFQ 跟 kyber 在吞吐量跟延迟上的表现要略逊一筹。而这些开销给 Linux 桌面场景下带来的提升量化后又有多少呢?这需要设计一个更加复杂的跑分测试了,我们需要让一个延迟敏感应用跟一个高吞吐量应用同时运行,然后对比不同 IO 调度器如何平衡两者的硬盘操作:

    # 低延迟小请求 (模拟交互式应用)
    fio --name=latency_sensitive \
        --filename=$TEST_DIR/latency_sensitive.fio \
        --size=2GB --direct=1 --rw=randread \
        --bs=4k --ioengine=libaio --iodepth=1 \
        --runtime=$RUNTIME --time_based \
        --group_reporting --output-format=json \
        --output=/tmp/latency_sensitive.json &
    
    # 高吞吐量请求 (模拟批处理)
    fio --name=throughput_heavy \
        --filename=$TEST_DIR/throughput_heavy.fio \
        --size=4GB --direct=1 --rw=randread \
        --bs=4k --ioengine=libaio --iodepth=512 \
        --runtime=$RUNTIME --time_based \
        --group_reporting --output-format=json \
        --output=/tmp/throughput_heavy.json &

    不同的 IO 调度器在我电脑上的实际测试结果如下:

    调度器延迟敏感应用 IOPS延迟敏感应用平均延迟高吞吐应用 IOPSIOPS 比率
    BFQ5,4740.18 ms129,35023.63
    kyber4,6110.22 ms138,90430.12
    none3,1540.32 ms146,10446.32

    IOPS 比率:高吞吐应用获得的 IOPS 是延迟敏感应用的多少倍

    通过上面的测试可以看到:

    1. BFQ 在公平性方面表现最佳,真正保护了延迟敏感应用
    2. None 调度器确实能提供最高的原始吞吐量,但以牺牲公平性为代价
    3. Kyber 提供了较好的平衡,但在极端公平性方面不如 BFQ

    结论

    正如 Arch Wiki 所说,如何选择 IO 调度器取决于你的使用场景跟工作负载,在我的工作电脑上,BFQ 才是那个能够让我这块中年硬盘提供最佳桌面应用使用体验的调度器。

  • 提升机械革命无界 15XPro 暴风雪的充电速度

    无界15XPro 的 99Wh 电池容量属实很大,但充电速度一言难尽,基本上要 4~5 小时左右才能从 10% 充到 100%。经过一番搜索后,总算找到了提升充电速度的方法。

    安装 Tuxedo Control Center 后,可以在 Settings 中找到 Battery Charging Options,只需要将 USB-C charging options 设置为 Priorize battery charing speed 就行。另外,如果长时间插电使用,还可以将 Charging profiles 设置成 Stationary use,这将会在硬件层面限制电池充电到 80% (但系统设置仍会显示充电到 100%)。

    如果你没安装 Tuxedo Control Center 也没关系,只要安装了驱动程序 (aur/mechrevo-drivers-dkms) ,就能通过下面两个 sysfs 接口实现同样的功能:

    cat /sys/bus/platform/drivers/tuxedo_keyboard/tuxedo_keyboard/charging_profile/charging_profile
    cat /sys/bus/platform/drivers/tuxedo_keyboard/tuxedo_keyboard/charging_priority/charging_prio
  • OpenWRT 上的 zerotier 突然无法启动了

    搭建自己的 homelab ,在局域网尽情冲浪~

    5 / 5 of homelab 历险记

    在我之前的文章中,介绍了我从公网远程访问家庭内网的方式,但在上个周末,这套配置出现了故障。故障的表现是 OpenWRT 上的 zerotier 客户端无法启动:

    zerotier-one[21492]: terminate called after throwing an instance of 'std::bad_cast'

    通过搜索得知这个故障通常伴随着硬盘问题,比如磁盘空间不足。通过 df -h 指令可以看到 /tmp 分区满了,而在 OpenWRT 上 /var 实际上会被链接到 /tmp 分区,这就导致 zerotier 客户端在启动时无法将配置写入 /var/lib/zerotier-one/networks.d 文件夹中。

    使用 du 命令,可以看到是 singbox 的日志文件充满了 /tmp 分区,强行删除日志文件后,虽然 df 命令可以看到空间被释放了,但实际上尝试写入 /var 仍然会报空间不足的错误。

    在 Gemini 的指点下,得知这很可能是因为仍然存在进程在使用这个文件,导致磁盘空间并没有被释放。果然,重启 singbox 进程后, zerotier 客户端终于能正常启动了。

    为了避免以后再有类似的情况出现,我干脆关闭了 singbox 的日志,等需要 debug 的时候再开启。

  • OBS 在 Linux 下启用 AMD GPU 加速

    OBS 在 Linux 平台下默认使用软件视频编码器,从而在录屏的时候造成 CPU 干活,GPU 摸鱼的场面。

    像我在使用的 AMD GPU,在正确安装驱动程序后,修改 OBS 设置中的 Output Mode 为 Advanced ,然后选择 FFmpeg VAAPI 作为视频编码器,这样就可以使用 GPU 编码视频了。

    可以看到, OBS 的 CPU 使用率大幅下降,并且 GPU 也在干活了。

  • claude code 使用心得

    一些常用的 /slash-commands

    /init

    让 claude code 到项目之前,最好先执行一下 /init,这样 claude code 就可以更加了解项目的功能以及编码习惯。不定期地在会话中执行 /init 也可以让 cc 及时了解项目的新规则,帮助它写出规范的代码。

    /compact

    用来压缩上下文,保留当前会话的记忆,但更省 token。

    /clear

    用来重置上下文,完全清楚当前会话的记忆,相当于重新开始会话。

    # new memory

    使用 # 开头就可以让 claude code 将你交代的事项记录在备忘录中,可以用来拓展 claude.md 中遗漏的项目约定或者你个人的习惯。

    向 claude code 布置任务的语言技巧

    我最常用的语言组织方式是这样的:

    描述现状

    要解决的问题

    关于解决这个问题的想法(如果有的话)

    任务大小最好控制在上面的描述可以在你能接受的范围内只用键盘完成输入,这样做的底层逻辑是人想要偷懒,会尽量少地用键盘打字,从而限制单个任务的大小,如果任务过于复杂,人出于打字上的懒惰,就不得不拆分任务。

    在我的实践下,这样交代任务对于 gemini-2.5-flash 跟 GLM-4.5 来说都会比较清晰且聚焦。

    手动压缩重置上下文

    由于 CC 完全不会有取舍地压缩上下文,所以,当我们准备开启一次新的任务时,最好手动压缩(/compact)或重置(/clear)上下文,这样做可以节省 Token 用量。

    何时压缩呢?当我们接下来的任务在代码文件上有切换,且跟前文有比较大的关联,但前文的要求没有被记录在 claude.md 或者 memory 中,压缩上下文就是比较好的选择。
    何时重置?当开始一个完全没有关联的新的任务时,我们可以选择重置上下文。

    模型能力对 Claude Code 的影响

    目前我中强度地测试了两种模型:GLM-4.5 跟 Gemini-2.5-Flash,前者价格只有 Claude Sonnet 4 的 1/10,后者更是只有 Claude Sonnet 4 的 1/50。

    从 Aider 的评测可以看到 claude sonnet 4 跟 gemini 2.5 flash 的性能差异并没有很大。

    而在实际的使用体验中,两者在修复构建脚本、CI 配置文件、升级 monorepo nx 的任务中,GLM-4.5 跟 Gemini-2.5-Flash 表现没有太多差异,都表现出了完整的 PDCA 行为,而且在细节执行层面也都表现出了一定的「倔强」性格。

    适合 Claude Code 的任务

    那些可以只在命令行完成验证任务最适合 claude code,这样 claude code 就可以通过执行命令的方式,验证任务是否完成,并且自动地在验证失败时调整解决方案。

  • 了解非加密哈希算法

    原始讨论见: https://v2ex.com/t/1145673

    我问 Gemini:

    帮助我理解下面的讨论:

    A:

    总共有关联 6 个问题,没找到答案,于是想一起问。

    1. 对于>256 字节的文件,对全文和尾部 128 位字节分别进行 xxHash3-128 然后拼接得到 256bit 散列和 SHA256 一次得到的 256bit 哪个更抗碰撞?
    2. 用于命名用户上传的文件,另外加盐用拼接还是 XOR 方式更好?
    3. 如果拼接的话,拼接在开头是不是要把原文 byte[]复制一遍,影响性能?
    4. 我知道应用于签名场景的话不能拼接在末尾,因为 SHA256 可以向末尾追加块,但是应用于标题这个场景是否安全(会增加暴露盐值让 hash 值可预测)?
    5. 如果通过将原文头部和盐 XOR 运算效果会不会更好?
    6. xxHash3 的 seed 可以代替盐吗?

    B:

    1. 把 xxHash3-128 和 SHA-256 对比还挺滑稽的,因为 xxHash3 从未为密码学设计,而安全性是全称命题(感性认知上属于不刻意设计就很可能不存在的性质)。
    2. 不理解什么是用 XOR 加盐,但我能想象的最简单的方式( XOR 消息之后散列,或者 XOR 散列值,或者两者都做)都很糟糕。
    3. 大多数 hash 算法都支持流式喂送数据,你只要调用两次名字类似 AppendData / Update 的方法就行了。
    4. 不太确定你想象中的“签名”是什么。如果你说的是 HMAC ,首先这是 MAC 而不是签名,然后 HMAC 和简单 hash 区别很大。
    5. 见 3.
    6. 见 1.
      另外,盐是散列函数索引 (hash key/index, or “seed” sometimes) 的实践表现,每个散列函数使用独立的 seed 从可证明安全角度自然没有“再加盐”的必要。

    Gemini 回答:

    好的,我们来深入解析一下这段对话。

    这段讨论的核心是:用户 A 试图通过“自己动手”的方式,将一个非加密哈希算法 (xxHash) 改造得“足够安全”,用于文件命名。而用户 B 则以一个更专业的安全视角,指出了这种做法的根本性错误和危险性。

    简单来说,A 在问“我该如何用木头造一辆跑车?”,而 B 在回答“你不应该用木头造跑车,你应该直接去买一辆真正的跑车。”

    下面我将逐条为你详细解释,并补充一些背景知识。


    核心思想解析

    • 用户 A 的目标: 为用户上传的文件生成一个唯一的、抗碰撞的、256位的标识符(文件名)。他希望兼顾 xxHash速度SHA256安全性
    • 用户 A 的方法: “自己动手,创造密码学”(Roll Your Own Crypto)。他想通过拼接、XOR、加盐等方式,把一个不安全的东西变得安全。
    • 用户 B 的观点: 这是密码学中的第一大忌。安全性是一个系统性的、需要严格数学证明的属性,不是通过简单的“混合”就能得到的。使用未经安全设计的算法来处理安全问题,从一开始就错了。

    逐条问题详解

    问题 1: xxHash3 拼接 vs. SHA256,哪个更抗碰撞?

    • A 的提议: hash = xxHash3_128(全文) + xxHash3_128(文件尾部)
    • B 的回答: “把 xxHash3-128 和 SHA-256 对比还挺滑稽的…安全性是全称命题。”
    • 专家解读:
      • B 的回答一针见血。这个问题的比较本身就没有意义。
      • SHA256 的抗碰撞性是密码学级别的。意思是,即使有一个拥有巨大算力的恶意攻击者,想主动找出两个不同的文件能产生同一个 SHA256 值,在计算上也是不可行的。
      • xxHash 的抗碰撞性是统计学级别的。意思是,对于随机的、非恶意的输入,它能很好地将它们散列到不同的值,以避免“意外”碰撞。但如果一个攻击者知道你用了 xxHash,他可以非常轻松地、在几毫秒内构造出无数个能产生相同 xxHash 值的文件。
      • A 的拼接方案安全性如何?它的安全性等同于其最薄弱的一环,也就是 xxHash。 攻击者完全可以忽略你的拼接手法,直接针对 xxHash 的弱点进行攻击。因此,A 的方案完全不具备密码学上的抗碰撞性

    问题 2 & 5: 加盐用拼接还是 XOR?

    • A 的提议: hash(盐 + 原文) 还是 hash(盐 XOR 原文)
    • B 的回答: “不理解什么是用 XOR 加盐…(但)都很糟糕。”
    • 专家解读:
      • B 的直觉是完全正确的。在密码学中,处理输入数据的标准方式是拼接,而不是 XOR。
      • 拼接 (Concatenation): hash(salt + message) 是相对标准和安全的方式(尤其是在盐是固定长度且放在前面时)。
      • XOR: hash(salt XOR message) 是非常危险的。如果 saltmessage 短,它会被重复使用,产生密码分析上的弱点。更糟糕的是,如果攻击者能以某种方式控制部分 message,他或许能抵消掉 salt 的作用。例如,如果文件的开头是一段固定的、已知的头部信息,攻击者就可以通过 XOR 操作推断或抵消盐值。永远不要用 XOR 来“混合”盐和消息。

    问题 3: 拼接的性能问题

    • A 的担忧: salt + message 是不是要把 message 的字节数组完整复制一遍,影响性能?
    • B 的回答: “大多数 hash 算法都支持流式喂送数据,你只要调用两次…Update 的方法就行了。”
    • 专家解读:
      • B 的回答是 100% 正确的,并指出了一个关键的工程实践。
      • 所有设计良好的哈希库都提供流式(Streaming)API。你不需要在内存里把盐和整个文件拼接成一个巨大的新对象。
      • 操作流程是这样的:
        1. 初始化一个哈希上下文(hasher)。
        2. 向 hasher “喂”入盐 (hasher.update(salt))。
        3. 接着向同一个 hasher “喂”入文件的内容,可以一块一块地喂,不需要一次性读入内存 (hasher.update(file_chunk_1), hasher.update(file_chunk_2), …)。
        4. 最后,从 hasher 中获取最终的哈希结果 (hasher.digest())。
      • 这种方式内存效率极高,无论文件有多大。

    问题 4: 盐放在末尾与签名

    • A 的担忧: 知道 hash(message + salt) 对于签名场景不安全(因为长度扩展攻击),那么在文件命名场景下是否安全?
    • B 的回答: “不确定你想象中的‘签名’是什么…如果你说的是 HMAC…区别很大。”
    • 专家解读:
      • A 了解到一个真实的攻击——长度扩展攻击 (Length Extension Attack),这值得肯定。这种攻击针对 hash(secret + message) 这种结构。
      • B 再次纠正了 A 的术语和理解。A 想要的不是“数字签名”(通常基于非对称加密),而是消息认证码 (MAC),它用于验证消息的完整性和真实性。
      • HMAC (Hash-based MAC) 是解决这个问题的标准方案。它的结构是 hash( (key XOR opad) + hash( (key XOR ipad) + message) )。这个复杂的结构就是为了彻底防御长度扩展攻击等问题。
      • 对于 A 的文件命名场景,虽然长度扩展攻击本身可能不是直接威胁,但这再次暴露了他试图“自己动手”解决一个已有标准方案的问题。正确做法就是使用 HMAC-SHA256。

    问题 6: xxHash3 的 seed 可以代替盐吗?

    • A 的提议: 用 xxHash 自带的 seed 功能是不是就不用自己加盐了?
    • B 的回答: “见 1…盐是散列函数索引…的实践表现…使用独立的 seed…没有‘再加盐’的必要。”
    • 专家解读:
      • B 的回答分为两层:
        1. “见 1”: 既然 xxHash 本身不安全,讨论如何“安全地”使用它的 seed 就没有意义了。这是根本问题。
        2. 但是,从理论上讲:B 的第二部分解释非常到位。在一个设计良好的哈希函数中,seedkey 参数就是实现“加盐”功能的正确方式。它被设计为算法初始状态的一部分,比手动在外部拼接更安全、更高效。
      • 所以,如果我们在使用一个密码学安全的哈希函数(比如 BLAKE3),那么使用它自带的 key 参数就是最理想的“加盐”方式,不需要再手动拼接。A 的问题本身是好的,只是用错了对象。

    总结与正确方案

    用户 A 的所有问题都源于一个错误的前提:试图用非安全工具解决安全问题。

    解决 A 的原始需求的正确方案是什么?

    他需要一个快速、安全、能加盐的哈希算法来为文件命名。

    方案一:行业标准 (The Gold Standard)

    使用 HMAC-SHA256。它绝对安全,被广泛应用和审查。速度对于单个文件命名来说完全足够。

    # 'secret_key' 应该是你保密的、随机生成的长字符串
    # 'input_file' 是用户上传的文件
    openssl dgst -sha256 -hmac "secret_key" input_file

    方案二:现代、高性能的选择 (The Best of Both Worlds)

    使用 BLAKE3。这是一个现代的加密哈希算法,它:

    1. 极其快速:速度与 xxHash 相当,远超 SHA256。
    2. 密码学安全:由顶级密码学家设计,安全性与 SHA-256 同级。
    3. 内置 keyed hashing 模式:这正是 A 想要的 seed 功能,是实现“加盐”的完美方式。

    这完美地满足了 A 对速度和安全性的所有要求。

  • 排查 duckdb-node-neo 并发查询时的随机段错误

    问题的说明跟复现请看这里,这篇文章主要记录一下如何排查 node addon 代码引发的问题。

    保存进程崩溃的翻车现场

    在 Linux/KDE 环境下,当应用进程崩溃的时候,systemd 会记录下该进程的翻车现场(aka,coredump)。我们可以使用 coredumpctl 这个工具查看当前机器上发生过的 coredump。

      List all captured core dumps:
    
          coredumpctl
    
      List captured core dumps for a program:
    
          coredumpctl list program
    
      Show information about the core dumps matching a program with `PID`:
    
          coredumpctl info PID
    
      Invoke debugger using the last core dump:
    
          coredumpctl debug
    
      Invoke debugger using the last core dump of a program:
    
          coredumpctl debug program
    
      Extract the last core dump of a program to a file:
    
          coredumpctl --output path/to/file dump program
    
      Skip debuginfod and pagination prompts and then print the backtrace when using `gdb`:
    
          coredumpctl debug --debugger-arguments "-iex 'set debuginfod enabled on' -iex 'set pagination off' -ex bt"

    还原翻车现场

    通过上面的步骤获取到 coredump 后,可以搭配 debugger 工具跟 symbol file 定位出问题的代码。我选择的 debugger 工具是 gdb 搭配 CLion 作 UI。这样只要将 node addon 项目用 CLion 打开,并导入 coredump 后,就可以获得一个功能齐全的 GUI Debugger。

    用 CLion 打开 node addon 项目

    以 duckdb-node-neo 为例,这个项目使用 node-gyp 作为构建工具,而 CLion 只支持 CMAKE 工程,所以需要手动创建一份 CMakefile 文件,让 IDE 的智能提示正常工具,方便在 debug 的过程中查看定义、搜索引用等。

    下面的这份 CMake 文件是我跟 Claude 一起编写的,不一定能够通过 IDE 编译 node addon,但是可以提供智能补全。

    cmake_minimum_required(VERSION 3.10)
    project(duckdb_node_neo)
    
    set(CMAKE_CXX_STANDARD 17)
    
    # include node headers
    include_directories(/usr/include/node)
    
    # Node.js addon API headers
    include_directories(${CMAKE_SOURCE_DIR}/bindings/node_modules/node-addon-api)
    
    # DuckDB headers
    include_directories(${CMAKE_SOURCE_DIR}/bindings/libduckdb)
    
    # Add source files
    file(GLOB SOURCE_FILES
        "bindings/src/*.cpp"
    )
    
    # This is just for CLion's code intelligence - not for actual building
    add_library(duckdb_node_bindings SHARED ${SOURCE_FILES})
    
    # Define preprocessor macros used in the code
    add_definitions(-DNODE_ADDON_API_DISABLE_DEPRECATED)
    add_definitions(-DNODE_ADDON_API_REQUIRE_BASIC_FINALIZERS)
    add_definitions(-DNODE_API_NO_EXTERNAL_BUFFERS_ALLOWED)
    
    # Add Node.js specific definitions to help CLion's IntelliSense
    add_definitions(-DNAPI_VERSION=8)
    
    # Tell CLion to treat this as a Node.js addon project
    set_target_properties(duckdb_node_bindings PROPERTIES
        CXX_STANDARD 17
        PREFIX ""
        SUFFIX ".node"
    )

    生成带有 symbol 的二进制

    设置后 debugger 后,还需要提供一个 symbol 文件,帮助 debugger 定位代码位置。对于 node-gyp 而言,只需要在编译时设置 --debug 参数即可生成带有 symbol 的二进制。

    cross-replace node-gyp configure --verbose --arch=$TARGET_ARCH --debug && node-gyp build --verbose --arch=$TARGET_ARCH --debug

    接下来我们只要用带有 symbol 的二进制复现进程崩溃的问题,就可以得到一个方便 debug 的 coredump。

    定位问题

    通过 debug coredump 文件,可以很容易找到抛出错误的地方,但这次很不走运的是, error 变量是 NULL,没有提供任何有价值的信息。另外,可以发现 result_ptr_ 似乎处于一个没有被初始化的状态。

    查看 duckdb 的源码后发现,虽然 duckdb 对外提供的是 C API,但是其本身是用 C++ 开发的,在将 C++ 操作以 C API 的形式暴露时,duckdb 会捕获内部的异常,然后将其中的错误信息写入到 duckdb_result 中。

    目前 coredump 表明 result_ptr_ 未被 duckdb 正常初始化,而异常对象很可能已经在跳出 catch 作用域时被销毁,现在只能在运行时 debug 才能捕获到具体的错误信息了。

    附加到进程上 debug

    CLion 提供了非常好用的附加到未启动进程上 debug 的功能,只需要向 CLion 提供进程的 commandline,它就可以自动将 debugger 附加到该进程上。

    在开始 debug 之前,还需要设置一下 C++ 异常断点:

    当启动问题进程,并复现崩溃后,就可以在 debugger 视图查看到实时的异常对象信息:

    这里的 obj 就是异常对象,但是由于 CLion 没有识别出异常的类型,我们得手动用 gdb 指令查看 obj 的信息。

    通过 gdb 的输出,可以看到异常对象的类型是 duckdb::InvalidInputException

    这时在变量页面输入下面的指令就行:

    *(duckdb::InvalidInputException*)obj

    终于,更加具体的错误信息被挖掘出来了~

    后续

    这个问题的根因是 duckdb 不支持在同一个 connection 对象上执行高并发查询,如果需要高并发,则应该为每个查询单独创建 connection,否则将会随机抛出异常 InvalidInputException

    @duckdb/node-api 没有正确初始化异常捕获对象,造成了野指针问题,这才让整个进程在异常报错出后产生段错误崩溃。

  • 早知道 zswap 这么配置就好了

    swap 对于 Linux Server 来说可能是一剂毒药,但对于我这种经常要打开几十个 Tab 的 Linux Desktop 用户来说则可能是救命稻草。网上关于 swap 的调优众说纷纭,我决定给这个充满争议的话题再添一些乱。

    简单来说,我发现使用完全不写入硬盘的 zswap 搭配一个 swap 文件(或分区)要比单纯只用 swap + zswap 要更加适合日常办公场景。

    zswap 是一种 swap 跟 RAM 中间的一个压缩池。当一块内存将被送进 swap 的时候,zswap 会将其先压缩然后放进一个单独的内存池中,而不是直接放进 swap 设备。当 zswap 内存池快满或者这块内存不适合压缩(比如压缩后体积变大了)的时候,zswap 则会将其直接放进 swap 设备。

    通过安装 aur/zswap-disable-writeback ,可以几乎禁用掉 zswap 将内存解压写入 swap 的行为,这时 zswap 就会像 zram/swap 一样工作——仅压缩内存到内存池,不写入到更慢的 swap 设备,例如,硬盘分区或者 swap 文件。

    这样做的好处就是 swap 设备可以空出来用于系统休眠,而且 zswap 仍可以给操作系统提供更多的内存空间,更高效的使用内存。

    比如,在一个有 32GB RAM 的设备上,默认预留 20% 的空间来创建 zswap 的内存池,假设压缩比在 4.0 左右,那么这台设备在理论上可以给应用程序分配的内存将高达 51.2GB ,立省一条 16GB 内存

    在上面的配置下,还可以将 swappiness 设置到 100 以上,让操作系统更加激进地将内存交换出 RAM,给当前正在运行的活跃操作空出更多的空间,以应对突发性的内存需求(比如,打开新的浏览器 Tab)。

    相较于只使用硬盘作为 swap 设备,禁用 writeback 的 zswap 还有经济性上的优势,只要我们的内存需求在大多数情况下能够被 zswap 的内存池满足(在上面的例子中大约是 51GB),就不会发生由 swap 引起的硬盘 IO,有助于延长硬盘的寿命。

  • 在机械革命无界 15XPro 暴风雪上运行 Linux

    最近把工作电脑换成了机械革命无界 15XPro 暴风雪,在收货后小小折腾了一两天后,终于让他大部分的功能工作正常了。

    键盘背光跟散热风扇的驱动

    可以参考这篇文章中的介绍安装来自德国一家公司维护的驱动。驱动安装好后,还可以安装类似鸡哥控制面板的 TUXEDO 控制面板,切换性能模式跟调整键盘背光 RGB:

    内置屏幕掉帧

    笔记本内置屏幕在 60Hz 时会频繁随机掉帧,这个现象跟 VRR 有关,需要强制启用 VRR。

    除此之外我还遇到了使用 Spectacle 截图时,外置显示器闪烁的问题,需要强制关闭外置显示器的 VRR。

    睡眠(suspend)后被立即唤醒

    当通过睡眠按键(Fn + F1)或者 systemctl suspend 进入睡眠状态时,系统会被立即唤醒,需要第二次尝试才能成功睡眠。

    反复阅读 https://wiki.archlinux.org/title/Power_management 系列文章后,我在 deepseek-r1 的帮助下成功地找出了这个问题的根因。

    TLDR

    创建一个 udev rule,禁用 PS/2 键盘的 Wakeup Trigger。

    # sudoedit /etc/udev/rules.d/99-disable-keyboard-wakeup.rules
    # Disable wakeup for PS/2 keyboard controller
    ACTION=="add", SUBSYSTEM=="serio", KERNEL=="serio0", ATTR{power/wakeup}="disabled"

    然后重载 udev rules:

    sudo udevadm control --reload-rules
    sudo udevadm trigger

    重启系统后验证是否生效:

    $ cat /sys/devices/platform/i8042/serio0/power/wakeup
    disabled

    现在尝试睡眠,就不会被立即唤醒了。

    排查过程

    首先安装 amd-debug-tools,运行 amd-s2idle test ,查看当前系统是否满足 s2idle 的要求:

    $ amd-s2idle test
    💻 AMD Ryzen AI 9 H 365 w/ Radeon 880M (family 1a model 24)
    💻 MECHREVO WUJIE Series (STX\KRK)
    🐧 Arch Linux
    🐧 Kernel 6.12.32-1-lts
    🔋 Battery BAT0 (OEM standard) is operating at 100.00% of design
    ✅ ASPM policy set to 'default'
    ✅ GPIO driver `pinctrl_amd` available
    ✅ PMC driver `amd_pmc` loaded (Program 11 Firmware 93.4.0)
    ✅ USB3 driver `xhci_hcd` bound to 0000:65:00.4, 0000:67:00.0, 0000:67:00.3, 0000:67:00.4
    ✅ USB4 driver `thunderbolt` bound to 0000:67:00.6
    ✅ System is configured for s2idle
    ✅ GPU driver `amdgpu` bound to 0000:65:00.0
    ✅ PC6 and CC6 enabled
    ✅ SMT enabled
    ✅ IOMMU properly configured
    ✅ ACPI FADT supports Low-power S0 idle
    🚦 Logs are provided via dmesg, timestamps may not be accurate over multiple cycles
    ✅ LPS0 _DSM enabled
    ✅ WLAN driver `mt7921e` bound to 0000:62:00.0
    ❌ Kernel is tainted: 12288
    💯 Your system does not meet s2idle prerequisites!
    🗣️ Explanations for your system
    🚦 Kernel is tainted
    A tainted kernel may exhibit unpredictable bugs that are difficult for this script to characterize. If this is intended behavior run the tool with --force. 
    For more information on this failure see:https://gitlab.freedesktop.org/drm/amd/-/issues/3089

    可以看到这里提示我的 kernel 被 tainted 了,这是安装 mechrevo-drivers-dkms 引起的,可以忽略。

    既然系统符合 s2idle 的要求,说明可能是某个设备在唤醒系统,deepseek 给出了接下来的排查思路,启用 PM 日志:

    # Enable verbose PM debugging
    echo 1 | sudo tee /sys/power/pm_print_times<br>echo 1 | sudo tee /sys/power/pm_debug_messages
    # Now try to suspend again
    systemctl suspend
    # Check dmesg after resume
    dmesg | grep -i "wake\|resume\|acpi"

    再次执行 systemctl suspend 复现问题后,可以在 dmesg 中看到下面的日志:

    [ 4636.873132] xhci_hcd 0000:67:00.4: PM: pci_pm_suspend_noirq returned 0 after 42252 usecs  
    [ 4636.873146] thunderbolt 0000:67:00.6: PM: pci_pm_suspend_noirq returned 0 after 42269 usecs  
    [ 4636.873169] pcieport 0000:00:08.3: PM: calling pci_pm_suspend_noirq @ 29723, parent: pci0000:00  
    [ 4636.878632] pcieport 0000:00:02.1: PM: pci_pm_suspend_noirq returned 0 after 10561 usecs  
    [ 4636.878653] pcieport 0000:00:03.2: PM: pci_pm_suspend_noirq returned 0 after 11025 usecs  
    [ 4636.885192] pcieport 0000:00:08.1: PM: pci_pm_suspend_noirq returned 0 after 12279 usecs  
    [ 4636.885300] pcieport 0000:00:08.3: PM: pci_pm_suspend_noirq returned 0 after 12119 usecs  
    [ 4636.885359] PM: noirq suspend of devices complete after 54.690 msecs  
    [ 4636.885376] ACPI: _SB_.PCI0.GPP5: LPI: Constraint not met; min power state:D1 current power state:D0  
    [ 4636.885381] ACPI: _SB_.PCI0.GPP6: LPI: Constraint not met; min power state:D1 current power state:D0  
    [ 4636.885388] ACPI: _SB_.PCI0.GPP4.SDCR: LPI: Constraint not met; min power state:D3hot current power state:D0  
    [ 4636.886012] PM: Triggering wakeup from IRQ 9  
    [ 4636.886504] ACPI: _SB_.PEP_: Successfully transitioned to state screen off  
    [ 4636.887420] ACPI: _SB_.PEP_: Successfully transitioned to state lps0 ms entry  
    [ 4636.887625] ACPI: _SB_.PEP_: Successfully transitioned to state lps0 entry  
    [ 4636.888555] PM: suspend-to-idle  
    [ 4636.888592] ACPI: EC: ACPI EC GPE status set  
    [ 4636.888623] ACPI: PM: Rearming ACPI SCI for wakeup  
    [ 4636.891279] PM: Triggering wakeup from IRQ 1  
    [ 4639.593124] amd_pmc: SMU idlemask s0i3: 0xffff1abd  
    [ 4639.593188] ACPI: PM: Wakeup unrelated to ACPI SCI  
    [ 4639.593189] PM: resume from suspend-to-idle  
    [ 4639.594699] amd_pmc AMDI000A:00: Last suspend didn't reach deepest state  
    [ 4639.595185] ACPI: _SB_.PEP_: Successfully transitioned to state lps0 exit  
    [ 4639.595760] PM: Triggering wakeup from IRQ 9  
    [ 4639.596746] ACPI: _SB_.PEP_: Successfully transitioned to state lps0 ms exit  
    [ 4639.597719] ACPI BIOS Error (bug): Could not resolve symbol [_SB.ACDC.RTAC], AE_NOT_FOUND (20240827/psargs-332)  
    [ 4639.597729] ACPI Error: Aborting method _SB.PEP._DSM due to previous error (AE_NOT_FOUND) (20240827/psparse-529)  
    [ 4639.597737] ACPI: _SB_.PEP_: Failed to transitioned to state screen on

    Deepseek 大胆推断是 IRQ 1 (typically the i8042 keyboard controller) 造成的问题,可以通过下面的方法来验证:

    echo "disabled" | sudo tee /sys/devices/platform/i8042/serio0/power/wakeup

    重试睡眠后,系统果然安然入睡了~

    进入 s2idle 睡眠后,一晚上大概会消耗 10% 左右的电量。

    Firefox 播放视频时会触发 GPU 图形崩溃

    当启用 Firefox 的硬件加速后,播放视频会让 GPU 崩溃,丢失全部的显存内容。还好这个问题已经被 AMD 修复,升级到最新的内核即可。

    色彩管理

    机械革命提供了官方的色彩文件,但必须要在 Windows 下安装机械革命控制台才能下载(官方客服并不受理此类咨询)。加载了官方的色彩配置文件后,可以比较明显地缓解低亮度下对比度过低的问题。为了方面读到这篇文章的你,点击下面的链接就可以下载到这份色彩配置文件~

    ipfs://QmaR5ZxFXNwcsjn9XZq6CpDfRFyoffDEr9fpi15Hmb31K7

    开机后有概率屏幕卡死

    开启自适应同步后可解决,感谢 LY 提供的解决方案。