2023年11月

D - Uninterruptible Sleep Process

认识 Linux ps 命令的状态列:

$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0 169156 12824 ?        Ss   Nov03   6:05 /lib/systemd/systemd 
root           2  0.0  0.0      0     0 ?        S    Nov03   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   Nov03   0:00 [rcu_gp]
root          14  0.0  0.0      0     0 ?        I    Nov03  10:34 [rcu_sched]
root          68  0.0  0.0      0     0 ?        SN   Nov03   0:00 [ksmd]
root         868  0.0  0.0   2812  1048 ?        Ss   Nov03   0:00 /usr/sbin/acpid
root         875  0.0  0.0 484180 15888 ?        Ssl  Nov03   5:39 /usr/sbin/NetworkManager
root         894  0.0  0.1 917300 28164 pts/0    Ssl+ Nov03   1:52 /usr/local/qualys/cloud-agent 
rtkit       1415  0.0  0.0 155920  2584 ?        SNsl Nov03   0:08 /usr/libexec/rtkit-daemon
supra       2445  0.0  0.0  41472  3876 ?        S<sl Nov03   0:00 /usr/bin/pipewire
supra    2176300  0.0  0.0  15472  4596 pts/2    R+   16:58   0:00 ps aux

从上面的输出可以看到 STAT (state) 列有各种字母组合的状态, 分别代表什么意思呢?

State 含义

https://man7.org/linux/man-pages/man1/ps.1.html

    the state of a process:
               D    uninterruptible sleep (usually IO)
               I    Idle kernel thread
               R    running or runnable (on run queue)
               S    interruptible sleep (waiting for an event to
                    complete)
               T    stopped by job control signal
               t    stopped by debugger during the tracing
               W    paging (not valid since the 2.6.xx kernel)
               X    dead (should never be seen)
               Z    defunct ("zombie") process, terminated but not
                    reaped by its parent

       For BSD formats and when the stat keyword is used, additional
       characters may be displayed:

               <    high-priority (not nice to other users)
               N    low-priority (nice to other users)
               L    has pages locked into memory (for real-time and
                    custom IO)
               s    is a session leader
               l    is multi-threaded (using CLONE_THREAD, like NPTL
                    pthreads do)
               +    is in the foreground process group

虽然上面的各种状态比较详细, 简化一下, 其实主要有: R, S, D, T, Z.
LinuxProcessStateChange.png

关于 T & t Stopped 的状态

从运行或可运行状态,我们可以使用SIGSTOP或SIGTSTP信号将进程置于停止状态(T)。两种信号之间的区别在于,我们使用编程方式发送SIGSTOP,例如运行 kill -STOP {pid} 命令。进程不能忽略此信号,并将进入停止状态。当我们使用键盘CTRL + Z发送SIGTSTP信号, 也可以把它置于T状态。与SIGSTOP不同,进程可以选择忽略此信号并在接收SIGTSTP后继续执行。

在此状态下,我们可以通过发送SIGCONT信号将进程带回运行或可运行状态。

关于 D Uninterruptible 的状态

An uninterruptible sleep state is a sleep state that will not handle a signal right away. It will wake only as a result of a waited-upon resource becoming available or after a time-out occurs during that wait (if specified when put to sleep). It is mostly used by device drivers waiting for disk or network IO (input/output).

When the process is sleeping uninterruptibly, signals accumulated during the sleep will be noticed when the process returns from the system call or trap.

You cannot kill "D" state processes, even with SIGKILL or kill -9. As the name implies, they are uninterruptible. You can only clear them by rebooting the server or waiting for the I/O to respond.

为什么需要 Uninterruptible 状态

对于某些必须在系统可能出现故障或意外断电的情况下确保数据完整性和一致性的系统进程。 例如,当文件系统正在将数据写入磁盘时,如果系统接收到信号或其他事件,则它不能简单地在操作中途停止。 数据必须完整写入,以确保不丢失或损坏。

可以使用不间断睡眠的进程的其他示例包括一些设备驱动程序、备份进程和虚拟内存操作。

简而言之,不间断睡眠用于确保关键系统进程能够完成其任务,而不会出现数据损坏或丢失的风险,即使在发生断电或系统崩溃等意外事件时也是如此。

TASK_KILLABLE state.

这是可中断睡眠进程和不可中断睡眠进程之间的折衷方案。 处于 TASK_KILLABLE 状态的进程仍然不能被通常意义上的中断(即不能强制系统调用返回 EINTR); 然而,处于这种状态的进程可以被终止。 这意味着,例如,通过 NFS 执行 I/O 的进程如果进入楔入状态,则可能会被终止。 并非所有系统调用都实现这种状态,因此某些系统调用仍然有可能卡住不可杀死的进程,但这肯定比以前的情况有所改进。

Cgroup freezer subsystem -> D

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-freezer
this subsystem suspends or resumes tasks in a cgroup.
什么情况下需要 cgroup freezer 子系统

  1. 暂停出问题的进程/线程, 查看它内存状态
  2. 搬家 checkpoint
  3. 暂停消耗资源(CPU/内存/网络)的进程/线程

使用cgroup 挂起进程的例子: https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt
与 SIGSTOP/SIGCONT 的区别:

  1. 进入的状态不同, SIGSTOP/SIGCONT 进入 stopped (T)状态, freezer 进入 Unineteruptiable sleep D.
  2. 被 SIGSTOP/SIGCONT 的进程能感应到信号, 被 freezer 的进程感应不到自己收到这个信号.

cgroup freezer 子系统和 SIGSTOP/SIGCONT 的区别?

进程的生命周期(from wikipidia):

pLifeCycle.webp

eBPF - 例子 进程在 CPU 的运行时间

probe 点: tracepoint:sched:sched_switch
查看参数:

supra@suprabox:~/work/ebpf/bpftrace$ sudo bpftrace -lv tracepoint:sched:sched_switch
tracepoint:sched:sched_switch
    char prev_comm[16]
    pid_t prev_pid
    int prev_prio
    long prev_state
    char next_comm[16]
    pid_t next_pid
    int next_prio

代码如下:

BEGIN
{
    printf("     usecs         count |      distribution                                  |");
}

tracepoint:sched:sched_switch { 
    $prev = args.prev_pid;
    if (@pid[$prev]) {
        @ns = hist((nsecs - @pid[$prev])/1000);
        delete(@pid[$prev]);
    }
    @pid[args.next_pid] = nsecs;
}

END
{
    clear(@pid);
}

BPF 例子 - 观测某个 tcp 连接的状态变化

给2个参数, 分别是 IP 和端口, 观察符合找个条件的tcp 连接的状态变化.

这个例子中本想同时对比 IP 和 port, 但是对于IP 遇到一个问题.

常见的 IPv4 是这么写的(字符串): 145.23.45.23
$sk->__sk_common.skc_daddr 拿到的地址 (unsigned int32): 0X91172d17.
使用 ntop 函数转换后是一个 inet_t, 虽然文档死硬说是 Sting 表示形式: inet_t = “145.23.45.23”

那么为了比较2个 IP 地址, 你想把 0X91172d17 转成 inet_t = “145.23.45.23”, 但是还不能比, 不能用 String 和 inet_t 比较.
要么把 145.23.45.23 通过 pton 转成 uint8[], 然后再通过强转 (uint32)pton("145.23.45.23")) 再和 0X91172d17 比较.

关键的关键是 pton 的参数一定要是 字符串常量, 变量不行, 因为它要在编译时知道类型. TMD.

#!/usr/bin/env bpftrace

#ifndef BPFTRACE_HAVE_BTF
#include <linux/socket.h>
#include <net/sock.h>
#else
#include <sys/socket.h>
#endif

BEGIN
{
    @host = "127.0.0.1";
    @port = (uint16)80;
    if ("" != str($1)) {
        @host = str($1);
    }
    if ("" != str($2)) {
        @port = (uint16)$2;
    }

    printf("looking for tcp connection related to : %s:%d\n", @host, @port);
    printf("%39s:%-6s %39s:%-6s %-10s -> %-10s\n", "src IP", "src port", "dest ip", "dest port", "old state", "new state");
    @states[1] = "ESTABLISHED";
    @states[2] = "SYN_SENT";
    @states[3] = "SYN_RECV";
    @states[4] = "FIN_WAIT1";
    @states[5] = "FIN_WAIT2";
    @states[6] = "TIME_WAIT";
    @states[7] = "CLOSE";
    @states[8] = "CLOSE_WAIT";
    @states[9] = "LAST_ACK";
    @states[10] = "LISTEN";
    @states[11] = "CLOSING";
    @states[12] = "NEW_SYN_RECV";
}

kfunc:vmlinux:tcp_set_state {
    $sk = ((struct sock *) args->sk);
    $inet_family = $sk->__sk_common.skc_family;
    if ($inet_family == AF_INET) {
      $daddr = ntop($sk->__sk_common.skc_daddr);
      $saddr = ntop($sk->__sk_common.skc_rcv_saddr);
    } else {
      $daddr = ntop($sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8);
      $saddr = ntop($sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8);
    }
    $lport = $sk->__sk_common.skc_num;
    $dport = $sk->__sk_common.skc_dport;

    $dport = bswap($dport);

    if (($dport == @port) || ( $lport == @port)) {
        $curState = @states[args.state];
        $key = str($dport);
        $oldState = @keyMap[$daddr, $key];
        if ($oldState == "") {
            $oldState = "NONE";
        }
        @keyMap[$daddr, $key] = $curState;

        printf("%39s:%-6d %39s:%-6d %-10s -> %-10s\n", $saddr, $lport, $daddr, $dport, $oldState, $curState);
    }
}

END {
    clear(@states);
    clear(@keyMap);
    clear(@host);
    clear(@port);
}

关于 PromQL 的 histogram_quantile 的算法

关于 PromQL 的 histogram_quantile 的算法. 很长一段时间内, 我都以为是使用正态分布算的, 其实并没有那么复杂.

假如我们有11个桶, 我们的数据是从0开始的, 第一个桶是接收落在(0~10]区间的数量, 第二个桶是接收落在(10 ~ 20]区间的数量, 以此类推, 第10个桶接收落在(90~100]区间的数量, 那么第11个桶接收(100 ~ +Inf) 的区间, 通常最后一个区间应该没有值或非常少.

那么如果要求第 95分位等值, 我们就要统计这时总共有多少samples, 其中第95th/100 落在那个桶. 假如我们收到100个samples, 那么第95th/100 就是第95个sample, 看它落在哪个桶. 如果我们收到10000个samples, 那就是第9500个sample, 看它落在哪个桶.

当我们看到它落到那个桶之后, 就在数一下这个桶共有多少个数, 然后算一下这个桶占据了第多少分位(低)到第多少分位(高), 所以就知道了这个桶占据了从多少分位到多少分位.

然后按照这个桶内的数据是平均分布的, 然后算出第nth(95th)是到底处于哪个值.

这篇问答很好的解释了这个算法是如何工作的:
https://stackoverflow.com/questions/55162093/understanding-histogram-quantile-based-on-rate-in-prometheus/69938239#69938239

官方的开源代码: https://github.com/prometheus/prometheus/blob/main/promql/quantile.go

其中要注意的是:

  1. 分位数值在桶内假定为线性分布进行插值计算
  2. 如果分位数落在最高的桶内,将返回第二高桶的上界值
  3. 如果最低桶的上界大于0,则假定自然下界为0

设计好桶是很关键的一步:

  1. 尽量在较低的桶内平均分布;
  2. 最大值不要超过第二高桶的上界;

对于上面提到的 如果分位数落在最高的桶内,将返回第二高桶的上界值, 下面是一个演示: 其中从 10240 到正无穷也有一些samples, 但是不论我们使用多少9999, 这里最多返回10240.

http_server_requests_duration_ms_bucket{le="5",method="GET",commandName="SigninLegacyView"} 0
http_server_requests_duration_ms_bucket{le="20",method="GET",commandName="SigninLegacyView"} 0
http_server_requests_duration_ms_bucket{le="80",method="GET",commandName="SigninLegacyView"} 30
http_server_requests_duration_ms_bucket{le="320",method="GET",commandName="SigninLegacyView"} 5881
http_server_requests_duration_ms_bucket{le="640",method="GET",commandName="SigninLegacyView"} 8567
http_server_requests_duration_ms_bucket{le="1280",method="GET",commandName="SigninLegacyView"} 8831
http_server_requests_duration_ms_bucket{le="2560",method="GET",commandName="SigninLegacyView"} 8865
http_server_requests_duration_ms_bucket{le="5120",method="GET",commandName="SigninLegacyView"} 8865
http_server_requests_duration_ms_bucket{le="10240",method="GET",commandName="SigninLegacyView"} 8867
http_server_requests_duration_ms_bucket{le="+Inf",method="GET",commandName="SigninLegacyView"} 8885

histogram.png