2024年5月

当 CPU 空闲(idle)的时候在干什么

今天学习 Linux schedule 框架的时候, 看到如下的图. 这个图列出了 Linux 内核里面的几个 schedule classes, 以及它们之间的关系.
图片源自于: https://deepdives.medium.com/digging-into-linux-scheduler-47a32ad5a0a8
1_IfRhjQssWLhgJqeItlUAdw.webp

图中各个方块分别表示 Linux 内核的几个 schedule class, 他们按照顺序通过链表的形式连在一起, 当前面的 schedule class 没有需要执行的task的时候, 就去下一个 schedule class 去查找. 当前面所有 schedule class 都没有 task 可以执行的时候, 就去执行 idle 这个虚拟的task.

各个 schedule class 的描述:

  1. Stop Scheduler (SCHED_STOP) - 停止调度器 这个调度类是用于管理特殊的停止任务,这些任务用于在CPU上停止所有其他任务的执行,通常用于实现CPU热插拔和一些系统级别的调试操作。这个调度类的任务具有最高的优先级,可以抢占系统中的任何其他任务。
  1. Deadline Scheduler (SCHED_DEADLINE) - 截止时间调度器 这是一种较新的调度类,它为任务提供了严格的截止时间保证。SCHED_DEADLINE使用了称为“恒定带宽算法”的调度策略,允许任务指定它们的运行周期、运行时间和截止时间。这个调度器试图保证每个任务在其截止时间之前都能完成指定的运行时间,适用于对时间敏感度非常高的任务。
  1. Real-Time Scheduler (RT) - 实时调度器 实时调度类分为两种策略:SCHED_FIFO(先进先出)和SCHED_RR(轮转)。这些策略用于实时任务,这些任务需要快速且可预测的响应时间。在SCHED_FIFO中,只有当运行的实时任务主动放弃CPU或者被更高优先级的实时任务抢占时,它才会停止运行。SCHED_RR与SCHED_FIFO类似,但是它为每个实时任务提供了时间片,使得同一优先级的实时任务可以轮流运行。
  1. Completely Fair Scheduler (CFS) - 完全公平调度器 CFS是Linux内核的默认调度类,用于普通的非实时任务。它的目标是确保所有运行的进程都能获得公平的CPU时间。CFS使用红黑树来跟踪所有可运行的进程,并尽量平等地分配CPU时间。它基于虚拟运行时间的概念,该时间考虑了进程的权重。权重越高的进程获得的CPU时间越多。
  1. Idle Scheduler (SCHED_IDLE) - 空闲调度器 这个调度类用于非常低优先级的任务,只有当系统中没有其他可运行的任务时,这些任务才会被调度执行。它通常用于那些可以在系统空闲时运行的后台任务,例如内存压缩守护进程(kcompactd)。

每个调度类都实现了一组调度器框架定义的通用接口,这些接口包括任务入队、出队、选择下一个运行任务等操作。调度器通过这些接口与调度类交互,以实现不同类型的任务调度需求。

Idle 调度器是如何实现的?

参考这篇文章: https://manybutfinite.com/post/what-does-an-idle-cpu-do/
最终是通过 IA 32 指令集的 HTL(halt) 指令来完成的.
hlt: Halt,使CPU进入halt状态,这是一种低功耗模式。在这种模式下,CPU会停止执行指令,直到下一个中断到来。当系统没有其他任务要执行时,操作系统会执行HLT指令来减少能耗和散热。

查询Linux的最新代码:
https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/irqflags.h#L48
asm volatile("sti": : :"memory");
这段代码的作用是使CPU开始响应外部中断,并且立即进入低功耗的halt状态,直到下一个中断到来。这通常用在系统空闲循环或者其他需要CPU等待事件的场合。由于这段代码会改变CPU的中断使能状态,并且可能影响内存的一致性.

在其他处理器架构中,可能有类似功能但名称不同的指令。例如,在ARM架构中,WFI(Wait for Interrupt)指令和WFE(Wait for Event)指令可以用来让处理器等待外部事件或中断。

为什么一个简单的 Java 进程的 core dump 那么大

写了一个非常简单的 java main 函数, 然后运行, 然后做 core dump, 竟然发现 core dump 竟然有 6.6G.

Java 程序:

public class ErrorExample {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(600000);
    }
}

做 core dump 和 2种不同的 heap dump.

gcore <pid>

$ ls -lah 
-rw-rw-r-- 1 supra supra 6.6G Apr 30 00:26 core.276268
-rw-rw-r-- 1 supra supra  67M Apr 30 00:26 core.276268.gz
-rw-rw-r-- 1 supra supra 1.3M Apr 30 01:02 heap.bin
-rw-rw-r-- 1 supra supra 334K Apr 30 01:02 heap.bin.gz
-rw------- 1 supra supra 2.8M Apr 30 01:01 heap.hprof
-rw------- 1 supra supra 855K Apr 30 01:01 heap.hprof.gz

上面分别是使用不同工具得到的不同 dump 及进一步压缩过后的:

  1. gcore 276268 得到 core.276268
  2. jhsdb jmap --binaryheap --pid 280038 得到 heap.bin
  3. jcmd 280038 GC.heap_dump heap.hprof 得到 heap.hprof.

可以看到 jhsdb 做出的最小, core dump 最大. 这很有可能是 JVM 保留了很多想用但是还没用到的内存.

查看JVM 当前使用的内存, 发现其实使用的很小:

$ jcmd 281746 GC.heap_info
281746:
 garbage-first heap   total 258048K, used 1744K [0x0000000707e00000, 0x0000000800000000)
  region size 1024K, 2 young (2048K), 0 survivors (0K)
 Metaspace       used 155K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 6K, capacity 386K, committed 512K, reserved 1048576K

然后查看使用的启动参数, 可以看到保留的最大的堆是将近4G(MaxHeapSize=4162846720), 保留的代码缓存区是240M(ReservedCodeCacheSize=251658240), 再加上其它, 比如 JDK 库, 元数据区等, 可能达到6G多.

$ jcmd 281746 VM.flags
281746:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=262144000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4162846720 -XX:MaxNewSize=2497708032 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

如果使用 pmap 查看这个进程的虚拟内存使用情况, 可以发现确实使用了6.6G(最后一行):

$ pmap -x 280038
280038:   java ErrorExample
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000707e00000  256000     328     328 rw---   [ anon ]
0000000717800000 2758656       0       0 -----   [ anon ]
00000007c0000000 1048576       0       0 -----   [ anon ]
............ 省略 ....................
00007f39c513e000       8       8       0 r---- ld-linux-x86-64.so.2
00007f39c5178000       8       8       8 rw--- ld-linux-x86-64.so.2
00007ffc854ed000     132      32      32 rw---   [ stack ]
ffffffffff600000       4       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB         6821276   37464   10660

Linux 文件特殊权限

最简单的文件看起:
例如:

$ ls -lah
total 948K
drwxrwxr-x  4 supra supra 4.0K Apr 19 21:02 .
drwxrwxr-x 14 supra supra 4.0K Dec  6 21:16 ..
-rw-rw-r--  1 supra supra   69 Nov  6  2022 Dockerfile
-rwxrwxr-x  1 supra supra  16K Apr 19 21:02 a.out
-rwxrwxr-x  1 supra supra  11K Sep 18  2021 hello
-rw-rw-r--  1 supra supra  217 Jun 14  2022 hello.c

读写执行权限

  1. 第一个字符表示文件类型:

    1. -:普通文件
    2. d:目录
    3. l:符号链接

    其他字符表示其他特殊类型的文件,如管道:p、套接字p等。

  2. 接下来的三个字符表示文件所有者(owner)的权限:

    1. r:读权限(可以查看文件内容)
    2. w:写权限(可以修改文件内容)
    3. x:执行权限(可以运行文件作为程序)
  3. 紧接着的三个字符表示文件所属组(group)的权限,含义同上。
  4. 最后三个字符表示其他用户(others)的权限,含义同上。
  5. 如果权限字符被替换为特殊字符,如sS,则表示设置了SUID或SGID位。如果是tT,则表示设置了粘滞位(sticky bit).

在Linux系统中,SUID(Set User ID)是一种特殊的文件权限设置,当应用于可执行文件时,它允许用户以文件所有者的身份运行该文件。通常,当一个程序运行时,它继承了启动它的用户的权限。但是,如果该程序具有SUID权限,那么不管是谁运行它,它都会以文件所有者的权限来执行。

然而,SUID也带来了潜在的安全风险。如果SUID程序存在漏洞,那么它可能被利用来提升权限,因此只有在确实需要时才应该设置SUID权限,并且需要确保相关程序是安全的。系统管理员应该定期审查具有SUID权限的文件,并确保它们来自可信的源并且是最新的。

SUID & SGID

SUID权限通常用八进制代码 4000 表示,或者在 ls -l 命令的输出中,可以看到在文件所有者的执行权限位上有一个 s 字符(例如 -rwsr-xr-x)。

SUID的意义在于它允许用户执行一些通常需要更高权限的操作,而不必给予用户那些权限。这是通过让特定的程序以更高权限(通常是root权限)运行来实现的。

为什么要这么做:

1. 安全性:SUID允许系统管理员授予用户执行特定高权限任务的能力,而不必给用户更广泛的权限。这有助于遵循最小权限原则,减少安全风险。

2. 功能性:某些程序需要访问系统资源或执行任务,这些资源或任务通常只能由系统管理员(root用户)访问或执行。例如,passwd 程序需要修改shadow 文件,该文件包含加密的用户密码,通常只有root用户才有权限修改它。

3. 用户便利性:SUID使得普通用户可以方便地执行某些需要特殊权限的操作,而无需切换到root用户。

Sticky权限

Sticky权限(也称为粘滞位或粘着位)是Linux和类Unix操作系统中的一种特殊权限设置,它可以应用于目录。当一个目录设置了sticky权限,这意味着只有文件的所有者、目录的所有者或者超级用户(root)才能删除或重命名目录中的文件。其他用户即使有该目录的写权限也不能删除或重命名其中不属于他们的文件。

Sticky权限通常用于共享目录,如/tmp,这个目录是所有用户都可以写入的。由于任何用户都可以创建临时文件,没有sticky权限的话,就可能出现一个用户删除或修改了另一个用户的文件的情况。通过设置sticky权限,系统管理员可以防止这种情况发生,确保用户只能管理自己的文件。

设置

有2种: 符号方式和数字方式

符号方式

chmod WhoWhatWhich file | directory
  1. Who - represents identities: u,g,o,a (user, group, other, all)
  2. What - represents actions: +, -, = (add, remove, set exact)
  3. Which - represents access levels: r, w, x, s, t(read, write, execute, SUID/SGID, sticky)

ex:

chmod ug+rw test.txt
chmod g+s 

数字方式

通常使用3位数字分别表示 owner, group, others.

chmod ### file | directory
  1. If the read permission should be set, add 4
  2. If the write permission should be set, add 2
  3. If the execute permission should be set, add 1
特殊权限

使用4位, 第一位代表特殊权限, 后面3位表示 owner, group, others.
SUID = 4
SGID = 2
Sticky = 1

chmod 2770 test.txt

urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

最近一个项目把 python 从 3.9 升级到3.11, 于是把 3.9 删除, 使用 brew install python@3.11, 其它都正常, 就是使用 requests 访问各个https 地址的时候, 就报错:

Failed to send a request to Slack API server: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)>

因为出错的是访问 slack api(其实跟访问那个url没关系), 所以找到了这个帖子: https://github.com/slackapi/bolt-python/issues/673.

可是里面提到的 Certificates.command 对于我的安装路径根本就不存在. 于是怀疑 brew 安装有问题, 重新安装, 没看到任何错误, 运行程序还少一样的错.

最终根据一篇帖子的描述, 找到了答案.

启动python, 运行下面的脚本:

supra@host tools % python -V
Python 3.11.9
supra@host tools % python
Python 3.11.9 (main, Apr  2 2024, 08:25:04) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import _ssl; _ssl.get_default_verify_paths()
('SSL_CERT_FILE', '/usr/local/etc/openssl@3/cert.pem', 'SSL_CERT_DIR', '/usr/local/etc/openssl@3/certs')

可以看到它用的证书在/usr/local/etc/openssl@3/cert.pem, 发现这个是一个软链接, 链接到 ../ca-certificates/cert.pem, 可是这个文件夹都不存在.

于是新建这个文件夹, 并且把正确的证书放到那里, 然后就工作了.

Java 应用产生 core dump

什么是core dump

在 Linux 系统中,当一个进程崩溃(例如,由于段错误或其他严重错误),它通常会产生一个称为 "核心转储"(core dump)的文件。核心转储文件包含了进程崩溃时的内存映像和一些执行上下文信息,如寄存器状态、程序计数器位置、内存管理信息等。这些信息对于开发者来说非常有价值,因为它们可以用来调试程序,了解程序崩溃的原因。

Java 进程如何产生 core dump

对于正在运行的 Java 进程, 它就是一个标准的linux 进程, 可以使用 linux 上的各种工具来产生 core dump, 比如 gcore 或者 kill :

$ gcore <pid>
$ kill -11 <pid>

对于 Java 应用程序自身, 当它奔溃的时候, 默认会产生 core dump, 因为它有如下默认参数-XX:+CreateCoredumpOnCrash:

$ java -XX:+PrintFlagsFinal -version | grep Core
     bool CreateCoredumpOnCrash                    = true                                      {product} {default}

虽然上面的方式都能产生 core dump, 但是很有可能你并不能看到 core dump, 因为有各种其它条件会阻碍 core dump的产生:

  1. 系统设置了 core size 的大小太小, 查看 ulimit -c
  2. core dump 要写入的文件夹没有权限
  3. core dump 被系统设置拦截, 比如 apport, 它产生了 crash report, 却拦截了 core dump的产生.

core dump 有什么用?

core dump 里有进程当时时间点上的全部内存信息, 寄存器信息, 栈信息, 栈上变量值, 打开文件句柄, 打开的socket 等各种非常有用的信息, 对于诊断应用为什么崩溃具有很大的意义.

gdb 可以直接打开core dump 文件, 并读取里面的信息.

$ gdb core core.720444` 

使用 gdb 去debug Java 程序比较麻烦.

但是可以使用 JDK 自带的 jdb:

# java 8 版本
$ jdb -listconnectors
$ jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:javaExecutable=$JAVA_HOME/bin/java,core=core.720444

java 11 版本
$ jhsdb clhsdb --exe ErrorExample --core core.720444

Core Dump vs Java Heap Dump

Core Dump

  • 定义
    Core dump 是操作系统在进程异常终止时生成的一个文件,它包含了进程终止时内存中的内容。
  • 内容
    Core dump 包含了进程的整个内存映像,包括程序计数器、寄存器、堆栈、堆内存、全局变量、打开的文件描述符、环境变量、程序代码、加载的共享库等。
  • 用途
    主要用于程序崩溃后的调试和故障排查。可以使用调试工具(如 gdb)来分析 core dump 文件,确定程序崩溃的原因。
  • 大小
    Core dump 文件通常很大,因为它包含了整个进程的内存映像。
  • 生成方式
    Core dump 通常由操作系统在进程崩溃时自动生成,或者可以使用 gcore 命令手动生成。

Java Heap Dump

  • 定义
    Java heap dump 是 JVM 在某一时刻的堆内存的快照,包含了所有的 Java 对象和类信息。
  • 内容
    Heap dump 专注于 Java 堆内存,包括对象实例、数组、类实例、垃圾收集器信息等。
  • 用途
    主要用于分析 Java 应用程序的内存使用情况,如检测内存泄漏、查看对象的分配和引用情况等。
  • 大小
    Heap dump 文件的大小取决于 Java 堆的大小,通常比完整的 core dump 小。
  • 生成方式
    Heap dump 可以通过 JVM 提供的工具(如 jmap)、管理接口(如 JMX)生成,或在发生 OutOfMemoryError 时自动生成(如果配置了 -XX:+HeapDumpOnOutOfMemoryError JVM 参数)。

heap dump vs crash log file

这就类似
core dump -> apport crash report
heap dump -> crash log file (err_log_pid.log)

-XX:OnError 选项

如果启动参数配置了-XX:OnError选项, 当 fatal error 产生的时候, JVM 就会执行该选项配置的命令. 多个执行命令可以用 ; 分割开. 可以用 %p 指代当前进程号, 因为 % 用作了特殊符, 所以遇到真的 %, 就要用两个 %% 代替. 官方给的例子:

java -XX:OnError="pmap %p" MyApp #使用 pmap 查看内存空间
java -XX:OnError="gcore %p; dbx - %p" MyApp # 使用 gcore 产生core dump 并用 dbx 进行debug
java -XX:OnError="gdb - %p" MyApp # 使用 gdb debug

-XX:ErrorFile 选项

设置当 fatal error 产生的时候, 把log写到哪个文件去, 需要全路径名(%p指代当前进程号, %%=%).
如果该文件存在, 并且可以写, 那么就覆盖.
如果不设置, 它的默认行为是: 产生一个 java_error_%p.log 文件, %p是进程号, 默认放在当前进程的当前工作目录(CWD), 如果当前工作目录不可用(比如写权限,空间不够等), 会写到临时文件夹目录(/tmp).
参考: https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/felog001.html

试验 -XX:OnError 和 -XX:ErrorFile

更多细节在:
https://www.tianxiaohui.com/index.php/Troubleshooting/Java-%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-fatal-error.html